
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("rawtypes")
public class Simulation<T extends Driver> {

    private Graph G; //Traffic network
    private Graph S; //Social network
    private ArrayList<T> D; //List of drivers

    private Control c;
    private ArrayList<Runner> R;

    private int iteration;
    private int episode;

    private long runtime;
    private List<Double> total_ttimes;
    private List<Double> AL_ttimes;
    private List<Double> AM_ttimes;
    private List<Double> BL_ttimes;
    private List<Double> BM_ttimes;

    public Simulation(Graph G, Graph S, ArrayList<T> D) {
        this.G = G;
        this.S = S;
        this.D = D;

        this.c = new Control();

    }

    @SuppressWarnings("deprecation")
    public void start() {

        //reset the simulation
        reset();

        runtime = System.currentTimeMillis();
        if (!Params.N_TIMES) {
            System.out.println("episode\te-deg\tavg\tAL\tAM\tBL\tBM");
        }

        //run the episodes
        Params.EG_EPSILON = Params.EG_EPSILON_DEFAULT;
        while (episode < Params.NUM_EPISODES) {
//            System.out.println("runing episode " + episode);
            runEpisode();
            Params.EG_EPSILON = Params.EG_EPSILON * Params.EG_DECAYRATE;

            if (!Params.N_TIMES) {
                System.out.print(episode);

                double average_tt = 0;
                double al = 0;
                double am = 0;
                double bl = 0;
                double bm = 0;

                for (Driver d : D) {
                    //d.printRoute();
                    if (d.getOrigin().getId().equals("A")) {
                        if (d.getDestination().getId().equals("L")) {
                            al += d.getTravelTime();
                        } else {
                            am += d.getTravelTime();
                        }

                    } else {
                        if (d.getDestination().getId().equals("L")) {
                            bl += d.getTravelTime();
                        } else {
                            bm += d.getTravelTime();
                        }
                    }

                    average_tt += d.getTravelTime();
                }
                System.out.print("\t" + Params.EG_EPSILON);
                average_tt = average_tt / D.size();
                System.out.print("\t" + (average_tt) * -1);
                System.out.print("\t" + (al / 600) * -1);
                System.out.print("\t" + (am / 400) * -1);
                System.out.print("\t" + (bl / 300) * -1);
                System.out.println("\t" + (bm / 400) * -1);

            }
        }

        runtime = System.currentTimeMillis() - runtime;

        //kill all threads
        for (Runner r : R) {
            r.stop();
        }

        if (Params.N_TIMES) {

            //print drivers' routes
            double average_tt = 0;
            double al = 0;
            double am = 0;
            double bl = 0;
            double bm = 0;

            for (Driver d : D) {
                //d.printRoute();
                if (d.getOrigin().getId().equals("A")) {
                    if (d.getDestination().getId().equals("L")) {
                        al += d.getTravelTime();
                       AL_ttimes.add(d.getTravelTime());
                    } else {
                        am += d.getTravelTime();
                        AM_ttimes.add(d.getTravelTime());
                    }

                } else {
                    if (d.getDestination().getId().equals("L")) {
                        bl += d.getTravelTime();
                        BL_ttimes.add(d.getTravelTime());
                    } else {
                        bm += d.getTravelTime();
                        BM_ttimes.add(d.getTravelTime());
                    }
                }

                average_tt += d.getTravelTime();
                total_ttimes.add(d.getTravelTime());
            }
            average_tt = average_tt / D.size();
            System.out.print("\t" + Math.abs(average_tt)+";"+desvioPadrao(total_ttimes));
            System.out.print("\t" + Math.abs(al / 600)+";"+desvioPadrao(AL_ttimes));
            System.out.print("\t" + Math.abs(am / 400)+";"+desvioPadrao(AM_ttimes));
            System.out.print("\t" + Math.abs(bl / 300)+";"+desvioPadrao(BL_ttimes));
            System.out.println("\t" + Math.abs(bm / 400)+";"+desvioPadrao(BM_ttimes));

            //((QLearningDriver)D.get(0)).printQTable();
            //D.get(0).printRoute();
        }
    }

    @SuppressWarnings("unchecked")
    private void reset() {
        total_ttimes = new ArrayList<>();
        AL_ttimes = new ArrayList<>();
        AM_ttimes = new ArrayList<>();
        BL_ttimes = new ArrayList<>();
        BM_ttimes = new ArrayList<>();
        Params.EG_EPSILON = Params.EG_EPSILON_DEFAULT;

        //create at most one thread per driver
        this.R = new ArrayList<Runner>();
        for (int n = 1; n <= Math.min(Params.NUM_THREADS, D.size()); n++) {
            Runner r = new Runner(D, c);
            R.add(r);
            r.start();
        }

        //current iteration
        this.iteration = 0;

        //current episode
        this.episode = 0;

    }

    private boolean runEpisode() {
		//return false if the stop criterion was achieved
        //if (<...>) return false;

        //-------------------------------
        episode++;
		//System.out.println("episode " + episode);

        //-------------------------------
        //reset some attributes before running episode
        iteration = 0;
        for (Driver d : D) {
            d.reset();
        }

        //-------------------------------
        while (iteration < Params.NUM_ITERATIONS) {
			//System.out.println("step "+iteration);

            //return false if "all agents arrived" stop criterium has been reached
            if (!step()) {
                break;
            }

        }

        /*if (episode > Params.NUM_EPISODES - 10) {
         for (Driver d : D) {
         d.printRoute();
         ((QLearningDriver)d).printQTable();
         }
         }*/
        return true;
    }

    private boolean step() {

        //-------------------------------
        //return false if all vehicles have arrived
        boolean finished = true;
        for (Driver d : D) {
            if (!d.hasArrived()) {
                finished = false;
            }
        }
        if (finished) {
            return false;
        }

        //-------------------------------
        iteration++;

        //-------------------------------
        //go_A
        c.a = -1;

        for (Runner r : R) {
            r.go_A();
        }

        boolean running = true;
        do {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            running = false;
            for (Runner r : R) {
                if (r.isRunning()) {
                    running = true;
                    break;
                }
            }
        } while (running);

        //-------------------------------
        //intermediate computation
        for (Edge e : G.getEdges()) {
            e.clearVehiclesHere();
        }
        for (T d : D) {

            if (!d.hasArrived()) {
                d.getCurrRoad().incVehiclesHere();
            }
        }
        for (Edge e : G.getEdges()) {
            e.updateTravelTime();
            e.updateWLUTravelTime();
        }

        //-------------------------------
        //go B
        c.a = -1;

        for (Runner r : R) {
            r.go_B();
        }

        running = true;
        do {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            running = false;
            for (Runner r : R) {
                if (r.isRunning()) {
                    running = true;
                    break;
                }
            }
        } while (running);

        return true;
    }

    public int getIteration() {
        return iteration;
    }

    public int getEpisode() {
        return episode;
    }

    //class to run agents
    public static class Runner<T extends Driver<T>> extends Thread {

        private ArrayList<T> D;
        private Control c;

        private boolean go_A;
        private boolean go_B;

        public Runner(ArrayList<T> D, Control c) {
            this.D = D;
            this.c = c;
            this.go_A = false;
            this.go_B = false;
        }

        //semaphore function
        public T getDriver() {
            synchronized (c) {
                do {
                    c.a++;
                } while (c.a < D.size() && D.get(c.a).hasArrived());
                if (c.a >= D.size()) {
                    return null;
                }
                return D.get(c.a);
            }
        }

        //whether the thread is running or not
        public boolean isRunning() {
            return go_A || go_B;
        }

        //start the first agents' procedure
        public void go_A() {
            this.go_A = true;
        }

        //start the second agents' procedure
        public void go_B() {
            this.go_B = true;
        }

        @Override
        public void run() {
            //keep running
            while (true) {

                //start to perform the drivers' steps when go is set true 
                if (this.go_A) {

                    T d = getDriver();
                    while (d != null) {

                        //run driver
                        d.step_A();

                        //wait before running the next driver
                        try {
                            sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        //get the next driver to run
                        d = getDriver();
                    }

                    this.go_A = false;

                    while (!this.go_B) {
                        try {
                            sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    d = getDriver();
                    while (d != null) {

                        //run driver
                        d.step_B();

                        //wait before running the next driver
                        try {
                            sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        //get the next driver to run
                        d = getDriver();
                    }

                    this.go_B = false;

                }

                try {
                    sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    //semaphore class to control the access to the agents list
    public static class Control {

        int a = -1;
    }

    public long getRuntime() {
        return this.runtime;
    }

    /**
     * Obtem o desvio padrão de uma Lista de numeros passados
     *
     * @param temposDetodosFluxos
     * @return
     */
    public double desvioPadrao(List<Double> objetos) {
        if (objetos.size() == 1) {
            return 0.0;
        } else {
            double mediaAritimetica = mediaAritimetica(objetos);
            double somatorio = 0l;
            for (int i = 0; i < objetos.size(); i++) {
                double result = objetos.get(i) - mediaAritimetica;
                somatorio = somatorio + result * result;
            }
            return Math.sqrt(((double) 1 / (objetos.size() - 1))
                    * somatorio);
        }
    }

    /**
     * Obtem o a media aritmetica de um array de Elementos
     *
     * @param temposDetodosFluxos
     * @return
     */
    public double mediaAritimetica(List<Double> objetos) {
        double somatorio = 0l;
        for (Double d : objetos) {
            somatorio += d;
        }
        return somatorio / objetos.size();
    }
}
